package com.hqd.ch03.v30.reader.xml.parse.spport;

import com.hqd.ch03.utils.DomUtils;
import com.hqd.ch03.v30.config.BeanDefinition;
import com.hqd.ch03.v30.config.ConstructorArgumentValues;
import com.hqd.ch03.v30.config.MutablePropertyValues;
import com.hqd.ch03.v30.factory.config.RuntimeBeanReference;
import com.hqd.ch03.v30.factory.config.TypedStringValue;
import com.hqd.ch03.v30.factory.support.*;
import com.hqd.ch03.v30.reader.xml.ParserContext;
import com.hqd.ch03.v30.reader.xml.namespace.NamespaceHandler;
import com.hqd.ch03.v30.reader.xml.namespace.NamespaceHandlerResolver;
import com.hqd.ch03.v30.reader.xml.namespace.support.DefaultNamespaceHandlerResolver;
import com.hqd.ch03.v30.reader.xml.parse.BeanDefinitionDocumentReader;
import com.hqd.ch03.v30.registry.BeanDefinitionRegistry;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.w3c.dom.CharacterData;
import org.w3c.dom.*;

import java.util.*;

public class DefaultBeanDefinitionDocumentReader implements BeanDefinitionDocumentReader {
    public static final String BEANS_NAMESPACE_URI = "http://www.springframework.org/schema/beans";
    public static final String META_ELEMENT = "meta";
    public static final String PROPERTY_ELEMENT = "property";
    public static final String TRUE_VALUE = "true";
    public static final String FALSE_VALUE = "false";
    public static final String CONSTRUCTOR_ARG_ELEMENT = "constructor-arg";
    public static final String INDEX_ATTRIBUTE = "index";
    public static final String TYPE_ATTRIBUTE = "type";
    public static final String DESCRIPTION_ELEMENT = "description";
    public static final String VALUE_ELEMENT = "value";
    public static final String NAME_ATTRIBUTE = "name";
    public static final String BEAN_ELEMENT = "bean";
    public static final String ID_ATTRIBUTE = "id";
    public static final String CLASS_ATTRIBUTE = "class";
    public static final String SCOPE_ATTRIBUTE = "scope";
    public static final String LAZY_INIT_ATTRIBUTE = "lazy-init";
    public static final String INIT_METHOD_ATTRIBUTE = "init-method";
    public static final String DESTROY_METHOD_ATTRIBUTE = "destroy-method";
    public static final String FACTORY_METHOD_ATTRIBUTE = "factory-method";
    public static final String FACTORY_BEAN_ATTRIBUTE = "factory-bean";
    public static final String REF_ATTRIBUTE = "ref";
    public static final String VALUE_ATTRIBUTE = "value";
    public static final String DEFAULT_VALUE = "default";
    public static final String ARRAY_ELEMENT = "array";
    public static final String PROP_ELEMENT = "prop";
    public static final String VALUE_TYPE_ATTRIBUTE = "value-type";
    public static final String KEY_TYPE_ATTRIBUTE = "key-type";
    public static final String LIST_ELEMENT = "list";
    public static final String MERGE_ATTRIBUTE = "merge";
    public static final String SET_ELEMENT = "set";
    public static final String PROPS_ELEMENT = "props";
    public static final String MAP_ELEMENT = "map";
    public static final String ENTRY_ELEMENT = "entry";
    public static final String KEY_ELEMENT = "key";
    public static final String KEY_ATTRIBUTE = "key";
    public static final String KEY_REF_ATTRIBUTE = "key-ref";
    public static final String VALUE_REF_ATTRIBUTE = "value-ref";
    private static final String SINGLETON_ATTRIBUTE = "singleton";
    protected NamespaceHandlerResolver resolver = new DefaultNamespaceHandlerResolver();

    @Override
    public void registerBeanDefinitions(Document doc, BeanDefinitionRegistry registry) {
        parseBeanDefinitions(doc.getDocumentElement(), registry);
    }

    /**
     * 解析xml标签为BeanDefinition
     *
     * @param root
     * @param registry
     */
    protected void parseBeanDefinitions(Element root, BeanDefinitionRegistry registry) {
        if (isDefaultNamespace(root)) {
            List<Element> childElements = DomUtils.getChildElements(root);
            childElements.forEach(element -> {
                /**
                 * 默认标签
                 */
                if (isDefaultNamespace(element)) {
                    processBeanDefinition(element, registry);
                } else {//自定义标签
                    parseCustomElement(element, registry);
                }
            });
        } else {
            parseCustomElement(root, registry);
        }
    }

    /**
     * 转换bean底下的自定义标签和属性
     *
     * @param ele
     * @param originalDef
     * @return
     */
    private BeanDefinition decorateBeanDefinitionIfRequired(
            Element ele, BeanDefinition originalDef) {
        NamedNodeMap attributes = ele.getAttributes();
        for (int i = 0; i < attributes.getLength(); i++) {
            Node node = attributes.item(i);
            originalDef = decorateIfRequired(node, originalDef);
        }
        NodeList children = ele.getChildNodes();
        for (int i = 0; i < children.getLength(); i++) {
            Node node = children.item(i);
            if (node.getNodeType() == Node.ELEMENT_NODE) {
                originalDef = decorateIfRequired(node, originalDef);
            }
        }
        return originalDef;
    }

    private BeanDefinition decorateIfRequired(Node node, BeanDefinition originalDef) {
        String namespaceURI = node.getNamespaceURI();
        if (!isDefaultNamespace(namespaceURI)) {
            NamespaceHandler handler = resolver.resolve(namespaceURI);
            if (handler != null) {
                return handler.decorate(node, originalDef);
            }
        }
        return originalDef;
    }

    /**
     * 处理bean标签
     *
     * @param ele
     * @param registry
     */
    private void processBeanDefinition(Element ele, BeanDefinitionRegistry registry) {
        BeanDefinition bd = doProcessBeanDefinition(ele, registry);
        /**
         * 解析自定义标签
         */
        BeanDefinition beanDefinition = decorateBeanDefinitionIfRequired(ele, bd);
        registry.registerBeanDefinition(bd.getId(), beanDefinition);
    }

    protected BeanDefinition doProcessBeanDefinition(Element ele, BeanDefinitionRegistry registry) {
        BeanDefinition bd = new BeanDefinition();
        parseBeanDefinitionAttributes(ele, bd);
        List<Element> childElements = DomUtils.getChildElements(ele);
        if (CollectionUtils.isNotEmpty(childElements)) {
            MutablePropertyValues pvs = new MutablePropertyValues();
            ConstructorArgumentValues cvs = new ConstructorArgumentValues();
            childElements.forEach(element -> {
                String tagName = element.getTagName();
                if (StringUtils.equalsIgnoreCase(PROPERTY_ELEMENT, tagName)) {
                    parsePropertyElement(element, pvs);
                } else if (StringUtils.equalsIgnoreCase(CONSTRUCTOR_ARG_ELEMENT, tagName)) {
                    parseConstructorArgElement(element, cvs);
                } else if (StringUtils.equalsIgnoreCase(DESCRIPTION_ELEMENT, tagName)) {
                    parseDescriptionElement(ele, bd);
                }
            });
            bd.setProperties(pvs);
            bd.setConstructorArgumentValues(cvs);
        }
        return bd;
    }

    private void parseBeanDefinitionAttributes(Element ele, BeanDefinition bd) {
        bd.setId(ele.getAttribute(ID_ATTRIBUTE));
        bd.setScope(ele.getAttribute(SCOPE_ATTRIBUTE));
        String lazyInit = ele.getAttribute(LAZY_INIT_ATTRIBUTE);
        bd.setLazyInit(isDefaultValue(lazyInit) ? true : Boolean.valueOf(lazyInit));
        bd.setBeanClass(ele.getAttribute(CLASS_ATTRIBUTE));
        String scope = ele.getAttribute(SCOPE_ATTRIBUTE);
        bd.setScope(StringUtils.isBlank(scope) ? SINGLETON_ATTRIBUTE : scope);
        bd.setInitMethodName(ele.getAttribute(INIT_METHOD_ATTRIBUTE));
        bd.setDestroyMethodName(ele.getAttribute(DESTROY_METHOD_ATTRIBUTE));
    }

    private void parseDescriptionElement(Element element, BeanDefinition bd) {
        StringBuilder sb = new StringBuilder();
        NodeList nl = element.getChildNodes();
        for (int i = 0; i < nl.getLength(); i++) {
            Node item = nl.item(i);
            if ((item instanceof CharacterData && !(item instanceof Comment)) || item instanceof EntityReference) {
                sb.append(item.getNodeValue());
            }
        }
        bd.setDescription(sb.toString());
    }

    private void parseConstructorArgElement(Element element, ConstructorArgumentValues cvs) {
        String indexStr = element.getAttribute(INDEX_ATTRIBUTE);
        String nameStr = element.getAttribute(NAME_ATTRIBUTE);
        String value = element.getAttribute(VALUE_ATTRIBUTE);
        ConstructorArgumentValues.ValueHolder valueHolder = new ConstructorArgumentValues.ValueHolder();
        if (StringUtils.isNotBlank(indexStr)) {
            try {
                int index = Integer.parseInt(indexStr);
                valueHolder.setIndex(index);
            } catch (NumberFormatException e) {
                throw new RuntimeException(element.getTagName() + " Attribute 'index' of tag 'constructor-arg' must be an integer");
            }
        }
        if (StringUtils.isNotBlank(nameStr)) {
            valueHolder.setName(nameStr);
        }
        if (StringUtils.isNotBlank(value)) {
            valueHolder.setValue(value);
        } else {
            value = element.getAttribute(REF_ATTRIBUTE);
            if (StringUtils.isBlank(value)) {
                throw new RuntimeException(" tag 'constructor-arg' must have 'ref' or 'value' attribute ");
            }
            valueHolder.setValue(value);
            valueHolder.setRef(true);
        }
        cvs.addConstructorValue(valueHolder);
    }

    /**
     * 转换bean下的property节点
     *
     * @param element
     * @return
     */
    private void parsePropertyElement(Element element, MutablePropertyValues propertyValues) {
        boolean hasValueAttr = element.hasAttribute(VALUE_ATTRIBUTE);
        boolean hasRefAttr = element.hasAttribute(REF_ATTRIBUTE);
        if (hasValueAttr && hasRefAttr) {
            throw new RuntimeException(element.getTagName()
                    + " is only allowed to contain either 'ref' attribute OR 'value' attribute OR sub-element");
        }
        String name = element.getAttribute(NAME_ATTRIBUTE);
        parsePropertyValue(element, propertyValues, name);
    }

    public void parsePropertyValue(Element ele, MutablePropertyValues propertyValues, String propertyName) {
        String elementName = (propertyName != null ?
                "<property> element for property '" + propertyName + "'" :
                "<constructor-arg> element");

        /**
         * 是否存在子元素
         */
        NodeList nl = ele.getChildNodes();
        Element subElement = null;
        for (int i = 0; i < nl.getLength(); i++) {
            Node node = nl.item(i);
            if (node instanceof Element && !nodeNameEquals(node, DESCRIPTION_ELEMENT) &&
                    !nodeNameEquals(node, META_ELEMENT)) {
                /**
                 * 子元素只能存在一个
                 */
                if (subElement != null) {
                    throw new RuntimeException(elementName + " must not contain more than one sub-element");
                } else {
                    subElement = (Element) node;
                }
            }
        }

        boolean hasRefAttribute = ele.hasAttribute(REF_ATTRIBUTE);
        boolean hasValueAttribute = ele.hasAttribute(VALUE_ATTRIBUTE);
        if ((hasRefAttribute && hasValueAttribute) ||
                ((hasRefAttribute || hasValueAttribute) && subElement != null)) {
            throw new RuntimeException(elementName +
                    " is only allowed to contain either 'ref' attribute OR 'value' attribute OR sub-element");
        }
        /**
         * 引用类型
         */
        if (hasRefAttribute) {
            String refName = ele.getAttribute(REF_ATTRIBUTE);
            if (StringUtils.isBlank(refName)) {
                throw new RuntimeException(elementName + " contains empty 'ref' attribute");
            }
            RuntimeBeanReference ref = new RuntimeBeanReference(refName);
            propertyValues.add(propertyName, ref);
        } else if (hasValueAttribute) {//值类型
            TypedStringValue valueHolder = new TypedStringValue(ele.getAttribute(VALUE_ATTRIBUTE));
            propertyValues.add(propertyName, valueHolder);
        } else if (subElement != null) {//子元素，集合，map之类的
            propertyValues.add(propertyName, parsePropertySubElement(subElement));
        } else {
            throw new RuntimeException(elementName + " must specify a ref or value");
        }
    }

    public Object parsePropertySubElement(Element ele) {
        return parsePropertySubElement(ele, null);
    }

    public Object parsePropertySubElement(Element ele, String defaultValueType) {
        if (nodeNameEquals(ele, VALUE_ELEMENT)) {
            return parseValueElement(ele, defaultValueType);
        } else if (nodeNameEquals(ele, ARRAY_ELEMENT)) {
            return parseArrayElement(ele);
        } else if (nodeNameEquals(ele, LIST_ELEMENT)) {
            return parseListElement(ele);
        } else if (nodeNameEquals(ele, SET_ELEMENT)) {
            return parseSetElement(ele);
        } else if (nodeNameEquals(ele, MAP_ELEMENT)) {
            return parseMapElement(ele);
        } else if (nodeNameEquals(ele, PROPS_ELEMENT)) {
            return parsePropsElement(ele);
        } else {
            throw new RuntimeException("Unknown property sub-element: [" + ele.getNodeName() + "]");
        }
    }

    public Map<Object, Object> parseMapElement(Element mapEle) {
        String defaultKeyType = mapEle.getAttribute(KEY_TYPE_ATTRIBUTE);
        String defaultValueType = mapEle.getAttribute(VALUE_TYPE_ATTRIBUTE);

        List<Element> entryEles = DomUtils.getChildElementsByTagName(mapEle, ENTRY_ELEMENT);
        ManagedMap<Object, Object> map = new ManagedMap<>(entryEles.size());
        map.setKeyTypeName(defaultKeyType);
        map.setValueTypeName(defaultValueType);
        map.setMergeEnabled(parseMergeAttribute(mapEle));

        for (Element entryEle : entryEles) {
            NodeList entrySubNodes = entryEle.getChildNodes();
            Element keyEle = null;
            Element valueEle = null;
            for (int j = 0; j < entrySubNodes.getLength(); j++) {
                Node node = entrySubNodes.item(j);
                if (node instanceof Element) {
                    Element candidateEle = (Element) node;
                    if (nodeNameEquals(candidateEle, KEY_ELEMENT)) {
                        if (keyEle != null) {
                            throw new RuntimeException("<entry> element is only allowed to contain one <key> sub-element");
                        } else {
                            keyEle = candidateEle;
                        }
                    } else {
                        if (nodeNameEquals(candidateEle, DESCRIPTION_ELEMENT)) {
                            // the element is a <description> -> ignore it
                        } else if (valueEle != null) {
                            throw new RuntimeException("<entry> element must not contain more than one value sub-element");
                        } else {
                            valueEle = candidateEle;
                        }
                    }
                }
            }

            Object key = null;
            boolean hasKeyAttribute = entryEle.hasAttribute(KEY_ATTRIBUTE);
            boolean hasKeyRefAttribute = entryEle.hasAttribute(KEY_REF_ATTRIBUTE);
            if ((hasKeyAttribute && hasKeyRefAttribute) ||
                    (hasKeyAttribute || hasKeyRefAttribute) && keyEle != null) {
                throw new RuntimeException("<entry> element is only allowed to contain either " +
                        "a 'key' attribute OR a 'key-ref' attribute OR a <key> sub-element");
            }
            if (hasKeyAttribute) {
                key = buildTypedStringValueForMap(entryEle.getAttribute(KEY_ATTRIBUTE), defaultKeyType, entryEle);
            } else if (hasKeyRefAttribute) {
                String refName = entryEle.getAttribute(KEY_REF_ATTRIBUTE);
                if (StringUtils.isBlank(refName)) {
                    throw new RuntimeException("<entry> element contains empty 'key-ref' attribute");
                }
                RuntimeBeanReference ref = new RuntimeBeanReference(refName);
                key = ref;
            } else if (keyEle != null) {
                key = parseKeyElement(keyEle, defaultKeyType);
            } else {
                throw new RuntimeException("<entry> element must specify a key");
            }

            // Extract value from attribute or sub-element.
            Object value = null;
            boolean hasValueAttribute = entryEle.hasAttribute(VALUE_ATTRIBUTE);
            boolean hasValueRefAttribute = entryEle.hasAttribute(VALUE_REF_ATTRIBUTE);
            boolean hasValueTypeAttribute = entryEle.hasAttribute(VALUE_TYPE_ATTRIBUTE);
            if ((hasValueAttribute && hasValueRefAttribute) ||
                    (hasValueAttribute || hasValueRefAttribute) && valueEle != null) {
                throw new RuntimeException("<entry> element is only allowed to contain either " +
                        "'value' attribute OR 'value-ref' attribute OR <value> sub-element");
            }
            if ((hasValueTypeAttribute && hasValueRefAttribute) ||
                    (hasValueTypeAttribute && !hasValueAttribute) ||
                    (hasValueTypeAttribute && valueEle != null)) {
                throw new RuntimeException("<entry> element is only allowed to contain a 'value-type' " +
                        "attribute when it has a 'value' attribute");
            }
            if (hasValueAttribute) {
                String valueType = entryEle.getAttribute(VALUE_TYPE_ATTRIBUTE);
                if (StringUtils.isBlank(valueType)) {
                    valueType = defaultValueType;
                }
                value = buildTypedStringValueForMap(entryEle.getAttribute(VALUE_ATTRIBUTE), valueType, entryEle);
            } else if (hasValueRefAttribute) {
                String refName = entryEle.getAttribute(VALUE_REF_ATTRIBUTE);
                if (StringUtils.isBlank(refName)) {
                    throw new RuntimeException("<entry> element contains empty 'value-ref' attribute");
                }
                RuntimeBeanReference ref = new RuntimeBeanReference(refName);
                value = ref;
            } else if (valueEle != null) {
                value = parsePropertySubElement(valueEle, defaultValueType);
            } else {
                throw new RuntimeException("<entry> element must specify a value");
            }

            map.put(key, value);
        }

        return map;
    }

    public Properties parsePropsElement(Element propsEle) {
        ManagedProperties props = new ManagedProperties();
        props.setMergeEnabled(parseMergeAttribute(propsEle));

        List<Element> propEles = DomUtils.getChildElementsByTagName(propsEle, PROP_ELEMENT);
        for (Element propEle : propEles) {
            String key = propEle.getAttribute(KEY_ATTRIBUTE);
            // Trim the text value to avoid unwanted whitespace
            // caused by typical XML formatting.
            String value = DomUtils.getTextValue(propEle).trim();
            TypedStringValue keyHolder = new TypedStringValue(key);
            TypedStringValue valueHolder = new TypedStringValue(value);
            props.put(keyHolder, valueHolder);
        }

        return props;
    }

    protected Object parseKeyElement(Element keyEle, String defaultKeyTypeName) {
        NodeList nl = keyEle.getChildNodes();
        Element subElement = null;
        for (int i = 0; i < nl.getLength(); i++) {
            Node node = nl.item(i);
            if (node instanceof Element) {
                // Child element is what we're looking for.
                if (subElement != null) {
                    throw new RuntimeException("<key> element must not contain more than one value sub-element");
                } else {
                    subElement = (Element) node;
                }
            }
        }
        if (subElement == null) {
            return null;
        }
        return parsePropertySubElement(subElement, defaultKeyTypeName);
    }

    protected final Object buildTypedStringValueForMap(String value, String defaultTypeName, Element entryEle) {
        try {
            TypedStringValue typedValue = buildTypedStringValue(value, defaultTypeName);
            return typedValue;
        } catch (ClassNotFoundException ex) {
            return value;
        }
    }

    public Set<Object> parseSetElement(Element collectionEle) {
        String defaultElementType = collectionEle.getAttribute(VALUE_TYPE_ATTRIBUTE);
        NodeList nl = collectionEle.getChildNodes();
        ManagedSet<Object> target = new ManagedSet<>(nl.getLength());
        target.setElementTypeName(defaultElementType);
        target.setMergeEnabled(parseMergeAttribute(collectionEle));
        parseCollectionElements(nl, target, defaultElementType);
        return target;
    }

    /**
     * 装换列表中的值标签
     *
     * @param ele
     * @param defaultTypeName
     * @return
     */
    public Object parseValueElement(Element ele, String defaultTypeName) {
        String value = DomUtils.getTextValue(ele);
        String specifiedTypeName = ele.getAttribute(TYPE_ATTRIBUTE);
        String typeName = specifiedTypeName;
        if (StringUtils.isBlank(typeName)) {
            typeName = defaultTypeName;
        }
        try {
            TypedStringValue typedValue = buildTypedStringValue(value, typeName);
            typedValue.setSpecifiedTypeName(specifiedTypeName);
            return typedValue;
        } catch (ClassNotFoundException ex) {
            return value;
        }
    }

    protected TypedStringValue buildTypedStringValue(String value, String targetTypeName)
            throws ClassNotFoundException {

        TypedStringValue typedValue;
        if (StringUtils.isBlank(targetTypeName)) {
            typedValue = new TypedStringValue(value);
        } else {
            typedValue = new TypedStringValue(value, targetTypeName);
        }
        return typedValue;
    }

    public Object parseArrayElement(Element arrayEle) {
        String elementType = arrayEle.getAttribute(VALUE_TYPE_ATTRIBUTE);
        NodeList nl = arrayEle.getChildNodes();
        ManagedArray target = new ManagedArray(elementType, nl.getLength());
        target.setElementTypeName(elementType);
        target.setMergeEnabled(parseMergeAttribute(arrayEle));
        parseCollectionElements(nl, target, elementType);
        return target;
    }

    public List<Object> parseListElement(Element collectionEle) {
        String defaultElementType = collectionEle.getAttribute(VALUE_TYPE_ATTRIBUTE);
        NodeList nl = collectionEle.getChildNodes();
        ManagedList<Object> target = new ManagedList<>(nl.getLength());
        target.setElementTypeName(defaultElementType);
        target.setMergeEnabled(parseMergeAttribute(collectionEle));
        parseCollectionElements(nl, target, defaultElementType);
        return target;
    }

    protected void parseCollectionElements(
            NodeList elementNodes, Collection<Object> target, String defaultElementType) {

        for (int i = 0; i < elementNodes.getLength(); i++) {
            Node node = elementNodes.item(i);
            if (node instanceof Element && !nodeNameEquals(node, DESCRIPTION_ELEMENT)) {
                target.add(parsePropertySubElement((Element) node, defaultElementType));
            }
        }
    }

    public boolean parseMergeAttribute(Element collectionElement) {
        String value = collectionElement.getAttribute(MERGE_ATTRIBUTE);
        return TRUE_VALUE.equals(value);
    }

    public boolean nodeNameEquals(Node node, String desiredName) {
        return desiredName.equals(node.getNodeName()) || desiredName.equals(getLocalName(node));
    }

    public String getLocalName(Node node) {
        return node.getLocalName();
    }

    /**
     * 调用自定义解析器
     *
     * @param ele
     * @param registry
     */
    private void parseCustomElement(Element ele, BeanDefinitionRegistry registry) {
        NamespaceHandler resolve = resolver.resolve(getNamespaceURI(ele));
        BeanDefinition bd = resolve.parse(ele, new ParserContext(registry));
        if (bd != null) {
            registry.registerBeanDefinition(bd.getId(), bd);
        }
    }

    /**
     * 判断是否是spring-bean的标签
     *
     * @param namespaceUri
     * @return
     */
    public boolean isDefaultNamespace(String namespaceUri) {
        return StringUtils.isBlank(namespaceUri) || BEANS_NAMESPACE_URI.equals(namespaceUri);
    }

    /**
     * 判断是否是spring-bean的标签
     *
     * @param element
     * @return
     */
    public boolean isDefaultNamespace(Element element) {
        return isDefaultNamespace(getNamespaceURI(element));
    }

    private boolean isDefaultValue(String value) {
        return StringUtils.isBlank(value) || DEFAULT_VALUE.equals(value);
    }

    public String getNamespaceURI(Node node) {
        return node.getNamespaceURI();
    }
}
